using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Linq;

namespace TodoSoft.Data.Migration
{
	public class Migrator
	{
		readonly private List<MigrationAdapter> _migrations = new List<MigrationAdapter> ();
		IVersionRepo _versionRepo;

        public Migrator(IEnumerable<Type> migrations)
            : this(new SqlCeVersionRepo(), typeof(VersionMigration))
        {
            foreach (var migration in migrations)
                AddMigration(migration);
        }

		public Migrator ():this(new SqlCeVersionRepo(), typeof(VersionMigration))
		{
			
		}
		
		internal Migrator (IVersionRepo version, Type versionMigrationType)
		{
			_versionRepo = version;
			AddMigration (versionMigrationType);
		}
		
		public void AddMigration (Type type)
		{
			var descriptor = new MigrationAdapter (type);
			_migrations.Add (descriptor);
		}

		/// <summary>
		/// Update current database to latest version.
		/// </summary>
		public void Migrate(IDataBase dataBase)
		{
			foreach (var migration in _migrations)
			{
				using (var sql = dataBase.OpenCommandProcessor())
				{
					sql.TransactionBegin ();
					try
					{
						var currentVersion = _versionRepo.GetCurrentVersion (sql, migration.ComponentId);
												
						foreach (var newVersion in migration.GetVersions ()
						         .Where(v => v.VersionNumber > currentVersion)
						         .OrderBy(v => v.VersionNumber))
						{
							newVersion.Apply (sql);
							_versionRepo.UpdateVersion (
								sql, 
								migration.ComponentId,
								migration.ComponentName,
								newVersion.VersionNumber,
								newVersion.VersionDescription);
						}
						sql.TransactionCommit ();
					} 
					catch (Exception exception)
					{
						Trace.WriteLine(exception.Message);
						sql.TransactionRollback ();
					}
				}
			}		
		}
	}
	
	internal class MigrationAdapter
	{
		public Type Type {get; private set;}
		public Guid ComponentId {get;private set;}
		public string ComponentName {get; private set;}
		
		public MigrationAdapter (Type type)
		{
			Type = type;
			var migrationAttributes = type.GetCustomAttributes (typeof(MigrationComponentAttribute), true);
			if (migrationAttributes.Length == 0)
				throw new Exception (string.Format (
					"The class {0} should be marked by {1}",
					type.Name,
					typeof(MigrationComponentAttribute).Name
				)
				);
			
			if (migrationAttributes.Length > 1)
			{
				throw new Exception (string.Format (
					"The class {0} should contains only one {1}",
					type.Name,
					typeof(MigrationComponentAttribute).Name
				)
				);
			}
			
			ComponentId = ((MigrationComponentAttribute)migrationAttributes [0]).Id;
			ComponentName = ((MigrationComponentAttribute)migrationAttributes [0]).Name;
			
			if (ComponentName == null)
			{
				ComponentName = this.GetType ().Name;
				
				//Trim 'Migration' at end string if exist.
				var migration = "Migration";
				var migrationIndex = ComponentName.LastIndexOf (migration);
				
				if (migrationIndex == ComponentName.Length - migration.Length)
					ComponentName = ComponentName.Substring (0, ComponentName.Length - migration.Length);
			}
		}

		public IEnumerable<VersionAdapter> GetVersions ()
		{
			foreach (var method in Type.GetMethods())
			{
				var attributes = method.GetCustomAttributes (typeof(ChangesetAttribute), true);
				if (attributes.Length == 1)
				{
					var attribute = (ChangesetAttribute)attributes [0];
					
					if (method.GetParameters ().Length > 0)
						throw new Exception ("The method " + method.Name + " should not have parameters");
					
					yield return new VersionAdapter(this, attribute, method);
				}
			}
		}
		
		public Migration CreateInstance()
		{
			return (Migration)Activator.CreateInstance (Type);
		}
	}
	
	internal class VersionAdapter
	{
		MigrationAdapter _parentMigration;
		MethodInfo _method;
		
		public int VersionNumber {get; private set;}
		public string VersionDescription {get; private set;}
		
		public VersionAdapter (
			MigrationAdapter parentMigration, 
			ChangesetAttribute attribute, 
			MethodInfo method)
		{
			_parentMigration = parentMigration;
			_method = method;
			
			VersionNumber = attribute.Version;
			VersionDescription = attribute.Description;
		}
		
		public void Apply (ISqlCommandProcessor sqlCommandProcessor)
		{
			var migrationInstance = _parentMigration.CreateInstance ();
			migrationInstance.DataBase = sqlCommandProcessor;
			
			_method.Invoke (migrationInstance, new object[0]);
		}
	}
}

